package cn.yeziji.forum.utils;

import cn.yeziji.forum.common.key.CommonKey;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 业务锁工具类
 *
 * @author hwy
 * @since 2023/06/20 11:56
 **/
@Component
@Slf4j
public class RedisLockUtils {
  private final ThreadLocal<LinkedList<String>> LOCK_FLAG_STACK = ThreadLocal.withInitial(LinkedList::new);
  @Resource
  private RedisTemplate redisTemplate;
  /**
   * 官方提供的Lua解锁脚本
   * <p><a href="http://redisdoc.com/string/set.html">...</a></p>
   */
  private static final String UNLOCK_LUA_SCRIPT;

  static {
    UNLOCK_LUA_SCRIPT = "if redis.call(\"get\",KEYS[1]) == ARGV[1] " +
            "then " +
            "    return redis.call(\"del\",KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end ";
  }

  /**
   * 上锁并重试
   *
   * @param key 键值
   * @param expire 过期时间
   * @param retryCount 重试次数
   * @param sleepMillis 每次重试的等待时间
   * @return {@link Boolean} 是否上锁成功
   */
  public boolean lock(String key, long expire, int retryCount, long sleepMillis) {
    boolean setResult = setRedisLock(key, expire);
    while((!setResult) && retryCount-- > 0) {
      try {
        log.info("lock failed, retrying...{}", retryCount);
        Thread.sleep(sleepMillis);
      } catch (InterruptedException e) {
        return false;
      }
      setResult = setRedisLock(key, expire);
    }
    return setResult;
  }

  /**
   * 追加锁
   *
   * @param key 键值
   * @param expire 过期时间
   * @return {@link Boolean} 是否追加成功，如果追加失败说明锁仍然被持有
   */
  public boolean setRedisLock(String key, long expire) {
    try {
      // 设置锁成功，返回'OK',否则返回null
      String naoId = NanoIdUtils.randomNaoId(18);
      LOCK_FLAG_STACK.get().push(naoId);
      return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(getLockKeyPrefix() + key, naoId, expire, TimeUnit.MILLISECONDS));
    } catch (Exception e) {
      log.error("set redis occured an exception", e);
    }
    return false;
  }

  /**
   * 执行 lua 脚本释放锁
   *
   * @param key 释放键值
   * @return {@link Boolean} 是否释放成功
   */
  public boolean releaseLock(String key) {
    // 释放锁的时候，有可能因为持锁之后方法执行时间大于锁的有效期，此时有可能已经被另外一个线程持有锁，所以不能直接删除
    try {
      String releaseValue = LOCK_FLAG_STACK.get().poll();
      if (StringUtils.isBlank(releaseValue)) {
        return false;
      }
      List<String> keys = new ArrayList<>();
      keys.add(getLockKeyPrefix() + key);
      // 使用lua脚本删除redis中匹配value的key，可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
      RedisScript<Boolean> script = new DefaultRedisScript<>(UNLOCK_LUA_SCRIPT, Boolean.class);
      Object execute = redisTemplate.execute(script, keys, releaseValue);
      return execute != null && (Boolean)execute;
    } catch (Exception e) {
      log.error("release lock occured an exception", e);
    }
    return false;
  }

  public String getLockKeyPrefix() {
    return CommonKey.REDIS_LOCK_PREFIX_KEY;
  }
}
